﻿// ------------------------------------------------------------------------
//  Copyright 2025 The Dapr Authors
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//      http://www.apache.org/licenses/LICENSE-2.0
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//  ------------------------------------------------------------------------

#nullable enable
using System;
using System.Net.Http;
using Dapr.Common.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
using Autogenerated = Dapr.Client.Autogen.Grpc.v1;

namespace Dapr.Common.Test.Extensions;

public sealed class DaprClientBuilderExtensionsTests
{
    [Fact]
    public void AddDaprClient_CallsConfigureAction()
    {
        var services = new ServiceCollection();
        var configurationMock = new Mock<IConfiguration>();
        var serviceProviderMock = new Mock<IServiceProvider>();
        serviceProviderMock.Setup(sp => sp.GetService(typeof(IConfiguration))).Returns(configurationMock.Object);

        var configureCalled = false;
        
        services.AddDaprClient<DaprTestClient, DaprTestGrpcClient, DaprTestBuilder, DaprTestClientBuilder>((
            _,
            _) =>
        {
            configureCalled = true;
        });

        var serviceProvider = services.BuildServiceProvider();
        var client = serviceProvider.GetRequiredService<DaprTestClient>();

        Assert.NotNull(client);
        Assert.True(configureCalled);
    }

    [Fact]
    public void AddDaprClient_ThrowsIfServicesIsNull()
    {
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
        IServiceCollection services = null;
#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.
        Assert.Throws<ArgumentNullException>(() =>
            // ReSharper disable once AssignNullToNotNullAttribute
#pragma warning disable CS8604 // Possible null reference argument.
            services.AddDaprClient<DaprTestClient, DaprTestGrpcClient, DaprTestBuilder, DaprTestClientBuilder>(
#pragma warning restore CS8604 // Possible null reference argument.
                (_, _) => { }, ServiceLifetime.Singleton));
    }
    
    [Fact]
    public void AddDaprClient_RegistersClientWithServiceCollection_Singleton()
    {
        var services = new ServiceCollection();
        var configurationMock = new Mock<IConfiguration>();
        var serviceProviderMock = new Mock<IServiceProvider>();
        serviceProviderMock.Setup(sp => sp.GetService(typeof(IConfiguration))).Returns(configurationMock.Object);

        services.AddDaprClient<DaprTestClient, DaprTestGrpcClient, DaprTestBuilder, DaprTestClientBuilder>((
            _,
            _) =>
        {
        }, ServiceLifetime.Singleton);

        var serviceProvider = services.BuildServiceProvider();
        var client1 = serviceProvider.GetRequiredService<DaprTestClient>();
        var client2 = serviceProvider.GetRequiredService<DaprTestClient>();

        Assert.NotNull(client1);
        Assert.NotNull(client2);
        Assert.Same(client1, client2); //Singletons should return the same instance
    }

    [Fact]
    public void AddDaprClient_RegistersClientWithServiceCollection_Scoped()
    {
        var services = new ServiceCollection();
        var configurationMock = new Mock<IConfiguration>();
        var serviceProviderMock = new Mock<IServiceProvider>();
        serviceProviderMock.Setup(sp => sp.GetService(typeof(IConfiguration))).Returns(configurationMock.Object);

        services.AddDaprClient<DaprTestClient, DaprTestGrpcClient, DaprTestBuilder, DaprTestClientBuilder>((
            _,
            _) =>
        {
        }, ServiceLifetime.Transient);

        var serviceProvider = services.BuildServiceProvider();
        var client1 = serviceProvider.GetRequiredService<DaprTestClient>();
        var client2 = serviceProvider.GetRequiredService<DaprTestClient>();

        Assert.NotNull(client1);
        Assert.NotNull(client2);
        Assert.NotSame(client1, client2); //Transient should return different instances
    }

    [Fact]
    public void AddDaprClient_RegistersClientWithServiceCollection_Transient()
    {
        var services = new ServiceCollection();
        var configurationMock = new Mock<IConfiguration>();
        var serviceProviderMock = new Mock<IServiceProvider>();
        serviceProviderMock.Setup(sp => sp.GetService(typeof(IConfiguration))).Returns(configurationMock.Object);

        services.AddDaprClient<DaprTestClient, DaprTestGrpcClient, DaprTestBuilder, DaprTestClientBuilder>((
            _,
            _) =>
        {
        }, ServiceLifetime.Scoped);

        var serviceProvider = services.BuildServiceProvider();

        using var scope1 = serviceProvider.CreateScope();
        var client1 = scope1.ServiceProvider.GetRequiredService<DaprTestClient>();
        var client2 = scope1.ServiceProvider.GetRequiredService<DaprTestClient>();
        
        Assert.NotNull(client1);
        Assert.NotNull(client2);
        Assert.Same(client1, client2); //Transient should return the same instance within the same scope

        using (var scope2 = serviceProvider.CreateScope())
        {
            var client3 = scope2.ServiceProvider.GetRequiredService<DaprTestClient>();
            Assert.NotNull(client3);
            Assert.NotSame(client1, client3); //Scoped should return different instances across different scopes
        }
    }
    
    private interface IDaprTestServiceBuilder : IDaprServiceBuilder;

    private sealed class DaprTestBuilder(IServiceCollection services) : IDaprTestServiceBuilder
    {
        /// <summary>
        /// The registered services on the builder.
        /// </summary>
        public IServiceCollection Services { get; } = services;
    }

    private sealed class DaprTestClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder<DaprTestClient>(configuration)
    {
        public override DaprTestClient Build()
        {
            throw new NotImplementedException();
        }
    }

    private abstract class DaprTestClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : IDaprClient
    {
        internal readonly HttpClient HttpClient = httpClient;
        internal readonly string? DaprApiToken = daprApiToken;
        internal Autogenerated.Dapr.DaprClient Client { get; } = client;
        
        public void Dispose()
        {
            // TODO release managed resources here
        }
    }

    private sealed class DaprTestGrpcClient(
        Autogenerated.Dapr.DaprClient client,
        HttpClient httpClient,
        string? daprApiToken = null) : DaprTestClient(client, httpClient, daprApiToken);
}
